/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.tools;
import java.io.*;
import java.util.*;
import java.util.logging.*;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import org.opensourcephysics.controls.*;
import org.opensourcephysics.display.OSPRuntime;
import org.opensourcephysics.display.PrintUtils;
import org.opensourcephysics.tools.Resource;
import org.opensourcephysics.tools.FontSizer;
import org.opensourcephysics.tools.ResourceLoader;
import org.opensourcephysics.tools.EncryptionTool;
import org.opensourcephysics.tools.JarTool;
import org.opensourcephysics.tools.JarTreeDialog;

/**
 * This provides a GUI for building LaunchNode and LaunchSet xml files.
 *
 * @author Douglas Brown
 * @version 1.2
 */
public class LaunchBuilder extends Launcher {

   // static constants
   static final Color RED = new Color(255, 102, 102);
   // static fields
   static JFileChooser fileChooser;
   static javax.swing.filechooser.FileFilter jarFileFilter;
   static javax.swing.filechooser.FileFilter htmlFileFilter;
   static javax.swing.filechooser.FileFilter allFileFilter;
   static int maxArgs = 4; // max number of main method args
   static File ospJarFolder;
   // instance fields
   Action newTabSetAction; // creates a new tabset with a single tab
   Action changeAction;    // loads input data into selected node
   Action newTabAction;    // creates a new root node and tab
   Action addAction;       // adds new node to selected node
   Action cutAction;       // cuts selected node to clipboard
   Action copyAction;      // copies selected node to clipboard
   Action pasteAction;     // adds clipboard node to selected node
   Action importAction;    // adds xml file node to selected node
   Action saveAsAction;    // saves selected node to a new file
   Action saveAction;      // saves selected node
   Action saveAllAction;   // saves tabset and all nodes
   Action saveSetAsAction; // saves tabset and all nodes to new files
   Action moveUpAction;    // moves selected node up
   Action moveDownAction;  // moves selected node down
   Action openJarAction;   // opens a jar file and sets node path
   Action searchJarAction; // searches the current jars for a launchable class
   Action saveJarAction;   // saves a jar file containing the current tab set
   Action openArgAction;   // opens an xml file and sets the current argument
   Action openURLAction;   // opens an html file and sets url
   Action openTabAction;   // opens a node in a new tab
   JTabbedPane editorTabs;
   FocusListener focusListener;
   KeyListener keyListener;
   JPanel displayPanel;
   JPanel launchPanel;
   JPanel authorPanel;
   ArrayList labels;
   JTextField titleField;
   JLabel titleLabel;
   JTextField passwordEditor;
   JLabel passwordLabel;
   JTextField nameField;
   JLabel nameLabel;
   JTextField tooltipField;
   JLabel tooltipLabel;
   TitledBorder htmlTitle;
   JTextField classField;
   JLabel classLabel;
   JTextField argField;
   JLabel argLabel;
   JSpinner argSpinner;
   JTextField jarField;
   JLabel jarLabel;
   JLabel htmlLabel;
   SpinnerNumberModel htmlSpinnerModel;
   JSpinner htmlSpinner;
   JLabel urlLabel;
   JTextField urlField;
   JLabel htmlTabTitleLabel;
   JTextField htmlTabTitleField;
   JPanel urlPanel;
   JTextPane descriptionPane;
   JScrollPane descriptionScroller;
   TitledBorder descriptionTitle;
   JEditorPane htmlPane;
   JScrollPane htmlScroller;
   JSplitPane displaySplitPane;
   JTextField authorField;
   JLabel authorLabel;
   JTextField keywordField;
   JLabel keywordLabel;
   JTextField levelField;
   JLabel levelLabel;
   JTextField languagesField;
   JLabel languagesLabel;
   JTextPane commentPane;
   JScrollPane commentScroller;
   TitledBorder commentTitle;
   TitledBorder optionsTitle;
   TitledBorder securityTitle;
   JCheckBox editorEnabledCheckBox;
   JCheckBox encryptCheckBox;
   JCheckBox onEditCheckBox;
   JCheckBox onLoadCheckBox;
   JCheckBox hideRootCheckBox;
   JCheckBox hiddenCheckBox;
   JCheckBox buttonViewCheckBox;
   JCheckBox singleVMCheckBox;
   JCheckBox showLogCheckBox;
   JCheckBox clearLogCheckBox;
   JCheckBox singletonCheckBox;
   JCheckBox singleAppCheckBox;
   JComboBox levelDropDown;
   String previousClassPath;
   JButton newTabButton;
   JButton addButton;
   JButton cutButton;
   JButton copyButton;
   JButton pasteButton;
   JButton moveUpButton;
   JButton moveDownButton;
   JMenuItem newItem;
   JMenuItem previewItem;
   JMenuItem saveNodeItem;
   JMenuItem saveNodeAsItem;
   JMenuItem saveSetAsItem;
   JMenuItem saveAllItem;
   JMenuItem saveJarItem;
   JMenuItem importItem;
   JMenuItem openTabItem;
   JMenu toolsMenu;
   JMenuItem encryptionToolItem;
   JToolBar toolbar;
   LaunchSaver saver = new LaunchSaver(this);
   static {
      JFrame.setDefaultLookAndFeelDecorated(true);
      JDialog.setDefaultLookAndFeelDecorated(true);
   }

   /**
    * No-arg constructor.
    */
   public LaunchBuilder() {
      OSPRuntime.setAuthorMode(true);
   }

   /**
    * Constructs a builder and loads the specified file.
    *
    * @param fileName the file name
    */
   public LaunchBuilder(String fileName) {
      super(fileName);
      OSPRuntime.setAuthorMode(true);
   }

   /**
    * Constructs a builder with or without a splash screen.
    *
    * @param splash true to show the splash screen
    */
   public LaunchBuilder(boolean splash) {
      super(splash);
      OSPRuntime.setAuthorMode(true);
   }

   /**
    * Constructs a builder and loads the specified file with or without splash.
    *
    * @param fileName the file name
    * @param splash true to show the splash screen
    */
   public LaunchBuilder(String fileName, boolean splash) {
      super(fileName, splash);
      OSPRuntime.setAuthorMode(true);
   }

   /**
    * Main entry point when used as application.
    *
    * @param args args[0] may be an xml file name
    */
   public static void main(String[] args) {
      //    java.util.Locale.setDefault(new java.util.Locale("es"));
      //    OSPLog.setLevel(ConsoleLevel.ALL);
      //    javax.swing.JFrame.setDefaultLookAndFeelDecorated(true);
      String fileName = null;
      if((args!=null)&&(args.length!=0)) {
         fileName = args[0];
      }
      LaunchBuilder builder = new LaunchBuilder(fileName);
      builder.frame.setVisible(true);
   }

   /**
    * Saves a node to the specified file.
    *
    * @param node the node
    * @param fileName the desired name of the file
    * @return the name of the saved file, or null if not saved
    */
   public String save(LaunchNode node, String fileName) {
      if(node==null) {
         return null;
      }
      if((fileName==null)||fileName.trim().equals("")) { //$NON-NLS-1$
         return saveAs(node);
      }
      // add .xml extension if none but don't require it
      if(XML.getExtension(fileName)==null) {
         while(fileName.endsWith(".")) { //$NON-NLS-1$
            fileName = fileName.substring(0, fileName.length()-1);
         }
         fileName += ".xml";             //$NON-NLS-1$
      }
      if(!saveOwnedNodes(node)) {
         return null;
      }
      String filepath = fileName;
      if (!tabSetBasePath.equals("")) { //$NON-NLS-1$
      	filepath = XML.getResolvedPath(fileName, tabSetBasePath);
      }
      else {
      	String jarBase = OSPRuntime.getLaunchJarDirectory();
      	filepath = XML.getResolvedPath(fileName, jarBase);
      }
      File file = new File(filepath);
      OSPLog.fine(fileName+" = "+file.getAbsolutePath()); //$NON-NLS-1$
      String fullName = XML.forwardSlash(file.getAbsolutePath());
      String path = XML.getDirectoryPath(fullName);
      XML.createFolders(path);
      XMLControlElement control = new XMLControlElement(node);
      control.write(fullName);
      if(!control.canWrite) {
         OSPLog.info(LaunchRes.getString("Dialog.SaveFailed.Message")+" "+fullName);                        //$NON-NLS-1$ //$NON-NLS-2$
         JOptionPane.showMessageDialog(null, LaunchRes.getString("Dialog.SaveFailed.Message")+" "+fileName, //$NON-NLS-1$ //$NON-NLS-2$
                                       LaunchRes.getString("Dialog.SaveFailed.Title"),                      //$NON-NLS-1$
                                       JOptionPane.WARNING_MESSAGE);
         return null;
      }
      node.setFileName(fileName);
      changedFiles.remove(node.getFileName());
      return fileName;
   }

   /**
    * Saves a node to an xml file selected with a chooser.
    *
    * @param node the node
    * @return the name of the file
    */
   public String saveAs(LaunchNode node) {
      getXMLChooser().setFileFilter(xmlFileFilter);
      if(node.getFileName()!=null) {
         String name = XML.getResolvedPath(node.getFileName(), tabSetBasePath);
         getXMLChooser().setSelectedFile(new File(name));
      } else {
         String name = node.name;
         if(name.equals(LaunchRes.getString("NewNode.Name"))||           //$NON-NLS-1$
                      name.equals(LaunchRes.getString("NewTab.Name"))) { //$NON-NLS-1$
            name = LaunchRes.getString("NewFile.Name");                  //$NON-NLS-1$
         }
         String path = XML.getResolvedPath(name+".xml", tabSetBasePath); //$NON-NLS-1$
         getXMLChooser().setSelectedFile(new File(path));
      }
      int result = getXMLChooser().showDialog(null, LaunchRes.getString("FileChooser.SaveAs.Title")); //$NON-NLS-1$
      if(result==JFileChooser.APPROVE_OPTION) {
         File file = getXMLChooser().getSelectedFile();
         // create folder structure
         String path = XML.forwardSlash(file.getParent());
         XML.createFolders(path);
         // check to see if file already exists
         if(file.exists()) {
            // prevent duplicate file names
            String name = XML.forwardSlash(file.getAbsolutePath());
            name = XML.getPathRelativeTo(name, tabSetBasePath);
            if(getOpenPaths().contains(name)) {
               JOptionPane.showMessageDialog(frame,
                                             LaunchRes.getString("Dialog.DuplicateFileName.Message")+" \""+name+"\"",  //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                                             LaunchRes.getString("Dialog.DuplicateFileName.Title"),                    //$NON-NLS-1$
                                             JOptionPane.WARNING_MESSAGE);
               return null;
            }
            int selected = JOptionPane.showConfirmDialog(frame, LaunchRes.getString("Dialog.ReplaceFile.Message")+" "+ //$NON-NLS-1$ //$NON-NLS-2$
               file.getName()+XML.NEW_LINE+LaunchRes.getString("Dialog.ReplaceFile.Question"),                         //$NON-NLS-1$
               LaunchRes.getString("Dialog.ReplaceFile.Title"),                                                        //$NON-NLS-1$
               JOptionPane.YES_NO_OPTION);
            if(selected!=JOptionPane.YES_OPTION) {
               return null;
            }
         }
         path = XML.forwardSlash(file.getAbsolutePath());
         String fileName = XML.getPathRelativeTo(path, tabSetBasePath);
         OSPRuntime.chooserDir = XML.getDirectoryPath(path);
         // get clones before saving
         Map clones = getClones(node);
         path = save(node, fileName);
         if(path!=null) {
            if(node.isRoot()) {
               // refresh title of root tab
               for(int i = 0; i<tabbedPane.getTabCount(); i++) {
                  LaunchPanel tab = (LaunchPanel) tabbedPane.getComponentAt(i);
                  if(tab.getRootNode()==node) {
                     tabbedPane.setTitleAt(i, node.toString());
                     break;
                  }
               }
            }
            // refresh clones
            for(Iterator it = clones.keySet().iterator(); it.hasNext(); ) {
               LaunchPanel cloneTab = (LaunchPanel) it.next();
               LaunchNode clone = (LaunchNode) clones.get(cloneTab);
               clone.setFileName(node.getFileName());
               // refresh title of clone tab
               if(clone==cloneTab.getRootNode()) {
                  int n = tabbedPane.indexOfComponent(cloneTab);
                  tabbedPane.setTitleAt(n, node.toString());
               }
            }
            if(tabSetName!=null) {
               changedFiles.add(tabSetName);
            }
         }
         return path;
      }
      return null;
   }

   // ______________________________ protected methods _____________________________

   /**
    * Saves the owned nodes of the specified node.
    *
    * @param node the node
    * @return true unless cancelled by user
    */
   protected boolean saveOwnedNodes(LaunchNode node) {
      if(node==null) {
         return false;
      }
      if(node.isSelfContained()) {
         return true; // owned nodes saved within node
      }
      LaunchNode[] nodes = node.getChildOwnedNodes();
      for(int i = 0; i<nodes.length; i++) {
         // save owned nodes of owned node, if any
         if(nodes[i].getChildOwnedNodes().length>1) {
            if(!saveOwnedNodes(nodes[i])) {
               return false;
            }
         }
         if(save(nodes[i], nodes[i].getFileName())==null) {
            return false;
         }
      }
      return true;
   }

   /**
    * Saves a tabset to a file selected with a chooser.
    *
    * @return the absolute path of the tabset file
    */
   protected String saveTabSetAs() {
      saver.setBuilder(this);
      saver.setVisible(true);
      if(!saver.isApproved()) {
         return null;
      }
      String fileName = XML.getResolvedPath(tabSetName, tabSetBasePath);
      File file = new File(fileName);
      if (file.exists()) {
        int selected = JOptionPane.showConfirmDialog(frame, LaunchRes.getString("Dialog.ReplaceFile.Message")+" "+ //$NON-NLS-1$ //$NON-NLS-2$
              file.getName()+XML.NEW_LINE+LaunchRes.getString("Dialog.ReplaceFile.Question"),                         //$NON-NLS-1$
              LaunchRes.getString("Dialog.ReplaceFile.Title"),                                                        //$NON-NLS-1$
              JOptionPane.YES_NO_OPTION);
        if(selected!=JOptionPane.YES_OPTION) {
          return null;
        }
      }
      saveState = saver.saveStateCheckBox.isSelected();
      return saveTabSet();
   }

   /**
    * Saves the current tabset.
    *
    * @return the full path to the saved file, or null if not saved
    */
   public String saveTabSet() {
      if(tabSetName == null) {
      	return null;
      }
      if (tabSetName.trim().equals("")) { //$NON-NLS-1$
         return saveTabSetAs();
      }
      // check for read-only files
      if(!isTabSetWritable()) {
         return saveTabSetAs();
      }
      if(!selfContained&&!saveTabs()) {
         return null;
      }
      // save tab set
    	String fileName = tabSetName;
      if (!tabSetBasePath.equals("")) { //$NON-NLS-1$
      	fileName = XML.getResolvedPath(tabSetName, tabSetBasePath);
      }
      else {
      	String jarBase = OSPRuntime.getLaunchJarDirectory();
      	fileName = XML.getResolvedPath(tabSetName, jarBase);
      }
      OSPLog.fine(fileName);
      File file = new File(fileName);
      fileName = XML.forwardSlash(file.getAbsolutePath());
      String path = XML.getDirectoryPath(fileName);
      XML.createFolders(path);
      LaunchSet tabset = new LaunchSet(this, tabSetName);
      XMLControlElement control = new XMLControlElement(tabset);
      if(control.write(fileName)==null) {
         return null;
      }
      changedFiles.clear();
      jarBasePath = null; // signals tabset is now a file
      if(spawner!=null) {
         spawner.open(fileName);
         spawner.refreshGUI();
      }
      return fileName;
   }

   /**
    * Saves tabs.
    * @return true unless cancelled by user
    */
   public boolean saveTabs() {
      Component[] tabs = tabbedPane.getComponents();
      for(int i = 0; i<tabs.length; i++) {
         LaunchPanel tab = (LaunchPanel) tabs[i];
         LaunchNode root = tab.getRootNode();
         if((root.getFileName()==null)||root.getFileName().equals("")) { //$NON-NLS-1$
            continue;
         }
         save(root, XML.getResolvedPath(root.getFileName(), tabSetBasePath));
      }
      return true;
   }

   /**
    * Refreshes the selected node.
    */
   protected void refreshSelectedNode() {
      refreshNode(getSelectedNode());
   }

   /**
    * Refreshes the specified node with data from the input fields.
    *
    * @param node the node to refresh
    */
   protected void refreshNode(LaunchNode node) {
      boolean changed = false;
      if(node!=null) {
         if(node.isSingleVM()!=singleVMCheckBox.isSelected()) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeVM"));                                           //$NON-NLS-1$
            LaunchNode parent = (LaunchNode) node.getParent();
            if((parent!=null)&&parent.isSingleVM()) {
               node.singleVM = false;
               node.singleVMOff = !singleVMCheckBox.isSelected();
            } else {
               node.singleVM = singleVMCheckBox.isSelected();
               node.singleVMOff = false;
            }
            if(node.isSingleVM()) {
               showLogCheckBox.setSelected(node.showLog);
               clearLogCheckBox.setSelected(node.clearLog);
               singleAppCheckBox.setSelected(node.isSingleApp());
            } else {
               singletonCheckBox.setSelected(node.singleton);
            }
            changed = true;
         }
         if(node.isSingleVM()&&(node.showLog!=showLogCheckBox.isSelected())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeShowLog"));                                      //$NON-NLS-1$
            node.showLog = showLogCheckBox.isSelected();
            changed = true;
         }
         if(node.isSingleVM()&&node.isShowLog()&&(node.clearLog!=clearLogCheckBox.isSelected())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeClearLog"));                                     //$NON-NLS-1$
            node.clearLog = clearLogCheckBox.isSelected();
            changed = true;
         }
         if(node.isSingleVM()&&(node.isSingleApp()!=singleAppCheckBox.isSelected())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeSingleApp"));                                    //$NON-NLS-1$
            LaunchNode parent = (LaunchNode) node.getParent();
            if((parent!=null)&&parent.isSingleApp()) {
               node.singleApp = false;
               node.singleAppOff = !singleAppCheckBox.isSelected();
            } else {
               node.singleApp = singleAppCheckBox.isSelected();
               node.singleAppOff = false;
            }
            changed = true;
         }
         if(node.singleton!=singletonCheckBox.isSelected()) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeSingleton"));                                    //$NON-NLS-1$
            node.singleton = singletonCheckBox.isSelected();
            changed = true;
         }
         if(node.hiddenInLauncher!=hiddenCheckBox.isSelected()) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeHidden"));                                       //$NON-NLS-1$
            node.hiddenInLauncher = hiddenCheckBox.isSelected();
            changed = true;
         }
         if(node.isButtonView()!=buttonViewCheckBox.isSelected()) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeButtonView"));                                       //$NON-NLS-1$
            node.setButtonView(buttonViewCheckBox.isSelected());
            changed = true;
         }
         if(!node.name.equals(nameField.getText())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeName"));                                         //$NON-NLS-1$
            node.name = nameField.getText();
            changed = true;
         }
         if(!node.tooltip.equals(tooltipField.getText())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeTooltip"));                                          //$NON-NLS-1$
            node.tooltip = tooltipField.getText();
            changed = true;
         }
         if(!node.description.equals(descriptionPane.getText())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeDesc"));                                         //$NON-NLS-1$
            node.description = descriptionPane.getText();
            changed = true;
         }
         int n = ((Integer) argSpinner.getValue()).intValue();
         String arg = argField.getText();
         if(!arg.equals("")) {                                                                                        //$NON-NLS-1$
            node.setMinimumArgLength(n+1);
         }
         if((node.args.length>n)&&!arg.equals(node.args[n])) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeArgs")+" "+n);                                   //$NON-NLS-1$ //$NON-NLS-2$
            node.args[n] = arg;
            if(arg.equals("")) {                                                                                      //$NON-NLS-1$
               node.setMinimumArgLength(1);
            }
            changed = true;
         }
         String jarPath = jarField.getText();
         if((jarPath.equals("")&&(node.classPath!=null))||(!jarPath.equals("")&&!jarPath.equals(node.classPath))) {   //$NON-NLS-1$ //$NON-NLS-2$
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodePath"));                                         //$NON-NLS-1$
            node.setClassPath(jarPath.equals("") ? null : jarPath);                                                   //$NON-NLS-1$
            changed = true;
         }
         n = ((Integer) htmlSpinner.getValue()).intValue();
         LaunchNode.HTML html = node.getHTML(n);
         String input = urlField.getText();
         if(input.equals("")) {          //$NON-NLS-1$
            input = null;
         }
         if((html==null)||(((html.path!=null)&&!html.path.equals(input))||((input!=null)&&!input.equals(html.path)))) {
            String title = (html==null) ? null : html.title;
            node.setHTML(n, title, input);
            changed = true;
         }
         input = htmlTabTitleField.getText();
         if(input.equals("")) {          //$NON-NLS-1$
            input = null;
         }
         if((html!=null)
             &&(((html.title!=null)&&!html.title.equals(input))||((input!=null)&&!input.equals(html.title)))) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeHTMLTabTitle"));                                 //$NON-NLS-1$
            node.setHTML(n, input, html.path);
            changed = true;
         }
         String className = classField.getText();
         if(className.equals("")) {                                                                                   //$NON-NLS-1$
            if(node.launchClassName!=null) {
               node.launchClassName = null;
               node.launchClass = null;
               OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeLaunchClass"));                               //$NON-NLS-1$
               changed = true;
            }
         } else if(!className.equals(node.launchClassName)||(!className.equals("")&&(node.getLaunchClass()==null))) { //$NON-NLS-1$
            boolean change = node.setLaunchClass(className);
            if(change) {
               OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeLaunchClass"));                               //$NON-NLS-1$
               changed = true;
            }
         }
         if(!node.getAuthor().equals(authorField.getText())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeAuthor"));                                       //$NON-NLS-1$
            node.author = authorField.getText();
            changed = true;
         }
         if(!node.keywords.equals(keywordField.getText())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeKeywords"));                                     //$NON-NLS-1$
            node.keywords = keywordField.getText();
            changed = true;
         }
         if(!node.level.equals(levelField.getText())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeLevel"));                                        //$NON-NLS-1$
            node.level = levelField.getText();
            changed = true;
         }
         if(!node.languages.equals(languagesField.getText())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeLanguages"));                                    //$NON-NLS-1$
            node.languages = languagesField.getText();
            changed = true;
         }
         if(!node.comment.equals(commentPane.getText())) {
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeComment"));                                      //$NON-NLS-1$
            node.comment = commentPane.getText();
            changed = true;
         }
         LaunchNode root = (LaunchNode) node.getRoot();
         if(root!=null) {
            boolean hide = hideRootCheckBox.isSelected();
            if(hide!=root.hiddenWhenRoot) {
               root.hiddenWhenRoot = hide;
               OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeRootHidden"));                                //$NON-NLS-1$
               changed = true;
            }
            boolean edit = editorEnabledCheckBox.isSelected();
            if(edit!=editorEnabled) {
               editorEnabled = edit;
               OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeEditorEnabled"));                             //$NON-NLS-1$
               if(tabSetName!=null) {
                  changedFiles.add(tabSetName);
               }
               refreshGUI();
            }
         }
         if(changed) {
            OSPLog.fine(LaunchRes.getString("Log.Message.ChangeNode")+" \""+node.toString()+"\"");                    //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            LaunchPanel tab = getSelectedTab();
            if(tab!=null) {
               tab.treeModel.nodeChanged(node);
            }
            if(node.getOwner()!=null) {
               changedFiles.add(node.getOwner().getFileName());
            } else {
               changedFiles.add(tabSetName);
            }
            refreshClones(node);
            refreshGUI();
         }
      }
   }

   /**
    * Creates a LaunchPanel with the specified root and adds it to a new tab.
    *
    * @param root the root node
    * @return true if tab successfully added
    */
   public boolean addTab(LaunchNode root) {
  	 if (root == null) return false;
      OSPLog.finest(root.toString());
      boolean added = super.addTab(root);
      if(added) {
         if(tabSetName==null) {
            tabSetName = LaunchRes.getString("Tabset.Name.New"); //$NON-NLS-1$
         }
         changedFiles.add(tabSetName);
         refreshGUI();
      }
      return added;
   }

   /**
    * Removes the selected tab. Overrides Launcher method.
    *
    * @return true if the tab was removed
    */
   public boolean removeSelectedTab() {
     // close the tabset if only one tab is open
     if(tabbedPane.getTabCount()==1) {
     	 String[] prevArgs = undoManager.getLauncherState();
       boolean removed = removeAllTabs();
       if(removed && prevArgs != null) {
         // null new args indicate not redoable
         LauncherUndo.LoadEdit edit = undoManager.new LoadEdit(null, prevArgs);
         undoSupport.postEdit(edit);
       }
       return removed;
     }
     // check for unsaved changes in the selected tab
     LaunchPanel tab = (LaunchPanel) tabbedPane.getSelectedComponent();
     if(tab!=null) {
       if(!saveChanges(tab)) {
         return false;
       }
     }
     boolean removed = super.removeSelectedTab();
     if(tabSetName!=null && removed) {
        changedFiles.add(tabSetName);
        refreshGUI();
     }
     return removed;
   }

   /**
    * Offers to save changes, if any, to the specified tab.
    *
    * @param tab the tab
    * @return true unless cancelled by user
    */
   protected boolean saveChanges(LaunchPanel tab) {
      LaunchNode root = tab.getRootNode();
      int n = tabbedPane.indexOfComponent(tab);
      String name = (n>-1) ? tabbedPane.getTitleAt(n) : getDisplayName(root.getFileName());
      boolean changed = changedFiles.contains(root.getFileName());
      LaunchNode[] nodes = root.getAllOwnedNodes();
      for(int i = 0; i<nodes.length; i++) {
         changed = changed||changedFiles.contains(nodes[i].getFileName());
      }
      if(changed) {
         int selected = JOptionPane.showConfirmDialog(frame,
            LaunchRes.getString("Dialog.SaveChanges.Tab.Message")+" \""+ //$NON-NLS-1$ //$NON-NLS-2$
            name+"\""+XML.NEW_LINE+                                      //$NON-NLS-1$
               LaunchRes.getString("Dialog.SaveChanges.Question"),       //$NON-NLS-1$
               LaunchRes.getString("Dialog.SaveChanges.Title"),          //$NON-NLS-1$
               JOptionPane.YES_NO_CANCEL_OPTION);
         if(selected==JOptionPane.CANCEL_OPTION) {
            return false;
         }
         if(selected==JOptionPane.YES_OPTION) {
            // save root and all owned nodes
            save(root, root.getFileName());
         }
      }
      return true;
   }

   /**
    * Removes all tabs and closes the tabset.
    *
    * @return true if all tabs were removed
    */
   protected boolean removeAllTabs() {
      if(!saveAllChanges()) {
         return false;
      }
      return super.removeAllTabs();
   }

   /**
    * Offers to save all changes, if any.
    *
    * @return true unless cancelled by user
    */
   protected boolean saveAllChanges() {
      // save changes to tab set
      if(!changedFiles.isEmpty()&&(tabbedPane.getTabCount()>0)) {
         String message = LaunchRes.getString("Dialog.SaveChanges.Tabset.Message"); //$NON-NLS-1$
         int selected = JOptionPane.showConfirmDialog(frame, message+"\""+          //$NON-NLS-1$
            tabSetName+"\""+XML.NEW_LINE+                                           //$NON-NLS-1$
               LaunchRes.getString("Dialog.SaveChanges.Question"),                  //$NON-NLS-1$
               LaunchRes.getString("Dialog.SaveChanges.Title"),                     //$NON-NLS-1$
               JOptionPane.YES_NO_CANCEL_OPTION);
         if(selected==JOptionPane.CANCEL_OPTION) {
            return false;
         }
         if(selected==JOptionPane.YES_OPTION) {
            if(tabSetName.equals(LaunchRes.getString("Tabset.Name.New"))||          //$NON-NLS-1$
                            !saveAllItem.isEnabled()) {
               saveTabSetAs();
            } else {
               saveTabSet();
            }
         }
      }
      return true;
   }

   /**
    * Refreshes string resources.
    */
   protected void refreshStringResources() {
      super.refreshStringResources();
      saver = new LaunchSaver(this);
      editorTabs.setTitleAt(0, LaunchRes.getString("Tab.Display"));                 //$NON-NLS-1$
      editorTabs.setTitleAt(1, LaunchRes.getString("Tab.Launch"));                  //$NON-NLS-1$
      editorTabs.setTitleAt(2, LaunchRes.getString("Tab.Author"));                  //$NON-NLS-1$
      htmlTitle.setTitle(LaunchRes.getString("Label.HTML"));                        //$NON-NLS-1$
      commentTitle.setTitle(LaunchRes.getString("Label.Comments"));                 //$NON-NLS-1$
      descriptionTitle.setTitle(LaunchRes.getString("Label.Description"));          //$NON-NLS-1$
      optionsTitle.setTitle(LaunchRes.getString("Label.Options"));                  //$NON-NLS-1$
      hiddenCheckBox.setText(LaunchRes.getString("Checkbox.Hidden"));               //$NON-NLS-1$
      buttonViewCheckBox.setText(LaunchRes.getString("Checkbox.ButtonView"));       //$NON-NLS-1$
      nameLabel.setText(LaunchRes.getString("Label.Name"));                         //$NON-NLS-1$
      tooltipLabel.setText(LaunchRes.getString("Label.Tooltip"));                   //$NON-NLS-1$
      htmlLabel.setText(LaunchRes.getString("Label.HTMLSpinner"));                  //$NON-NLS-1$
      htmlTabTitleLabel.setText(LaunchRes.getString("Label.HTMLTabTitle"));         //$NON-NLS-1$
      urlLabel.setText(LaunchRes.getString("Label.URL"));                           //$NON-NLS-1$
      jarLabel.setText(LaunchRes.getString("Label.Jar"));                           //$NON-NLS-1$
      classLabel.setText(LaunchRes.getString("Label.Class"));                       //$NON-NLS-1$
      argLabel.setText(LaunchRes.getString("Label.Args"));                          //$NON-NLS-1$
      singleVMCheckBox.setText(LaunchRes.getString("Checkbox.SingleVM"));           //$NON-NLS-1$
      showLogCheckBox.setText(LaunchRes.getString("Checkbox.ShowLog"));             //$NON-NLS-1$
      clearLogCheckBox.setText(LaunchRes.getString("Checkbox.ClearLog"));           //$NON-NLS-1$
      singletonCheckBox.setText(LaunchRes.getString("Checkbox.Singleton"));         //$NON-NLS-1$
      singleAppCheckBox.setText(LaunchRes.getString("Checkbox.SingleApp"));         //$NON-NLS-1$
      authorLabel.setText(LaunchRes.getString("Label.Author"));                     //$NON-NLS-1$
      keywordLabel.setText(LaunchRes.getString("Label.Keywords"));                  //$NON-NLS-1$
      levelLabel.setText(LaunchRes.getString("Label.Level"));                       //$NON-NLS-1$
      languagesLabel.setText(LaunchRes.getString("Label.Languages"));               //$NON-NLS-1$
      securityTitle.setTitle(LaunchRes.getString("Label.Security"));                //$NON-NLS-1$
      editorEnabledCheckBox.setText(LaunchRes.getString("Checkbox.EditorEnabled")); //$NON-NLS-1$
      encryptCheckBox.setText(LaunchRes.getString("Checkbox.Encrypted"));           //$NON-NLS-1$
      passwordLabel.setText(LaunchRes.getString("Label.Password"));                 //$NON-NLS-1$
      onLoadCheckBox.setText(LaunchRes.getString("Checkbox.PWLoad"));               //$NON-NLS-1$
      titleLabel.setText(LaunchRes.getString("Label.Title"));                       //$NON-NLS-1$
      hideRootCheckBox.setText(LaunchRes.getString("Checkbox.HideRoot"));           //$NON-NLS-1$
      previewItem.setText(LaunchRes.getString("Menu.File.Preview"));                //$NON-NLS-1$
      encryptionToolItem.setText(LaunchRes.getString("MenuItem.EncryptionTool"));   //$NON-NLS-1$
      newItem.setText(LaunchRes.getString("Menu.File.New"));                        //$NON-NLS-1$
      importItem.setText(LaunchRes.getString("Action.Import"));                     //$NON-NLS-1$
      saveNodeItem.setText(LaunchRes.getString("Action.SaveNode"));                 //$NON-NLS-1$
      saveNodeAsItem.setText(LaunchRes.getString("Action.SaveNodeAs"));             //$NON-NLS-1$
      saveAllItem.setText(LaunchRes.getString("Action.SaveAll"));                   //$NON-NLS-1$
      openTabItem.setText(LaunchRes.getString("Action.OpenTab"));                   //$NON-NLS-1$
      saveSetAsItem.setText(LaunchRes.getString("Action.SaveSetAs"));               //$NON-NLS-1$
      toolsMenu.setText(LaunchRes.getString("Menu.Tools"));                         //$NON-NLS-1$
      newTabButton.setText(LaunchRes.getString("Action.New"));                      //$NON-NLS-1$
      addButton.setText(LaunchRes.getString("Action.Add"));                         //$NON-NLS-1$
      cutButton.setText(LaunchRes.getString("Action.Cut"));                         //$NON-NLS-1$
      copyButton.setText(LaunchRes.getString("Action.Copy"));                       //$NON-NLS-1$
      pasteButton.setText(LaunchRes.getString("Action.Paste"));                     //$NON-NLS-1$
      moveUpButton.setText(LaunchRes.getString("Action.Up"));                       //$NON-NLS-1$
      moveDownButton.setText(LaunchRes.getString("Action.Down"));                   //$NON-NLS-1$
      // reset label sizes
      labels.clear();
      labels.add(nameLabel);
      labels.add(tooltipLabel);
      labels.add(htmlLabel);
      // set label sizes
      FontRenderContext frc = new FontRenderContext(null, // no AffineTransform
         false,                                           // no antialiasing
         false);                                          // no fractional metrics
      Font font = nameLabel.getFont();
      //display panel labels
      int w = 0;
      for(Iterator it = labels.iterator(); it.hasNext(); ) {
         JLabel next = (JLabel) it.next();
         Rectangle2D rect = font.getStringBounds(next.getText()+" ", frc); //$NON-NLS-1$
         w = Math.max(w, (int) rect.getWidth()+1);
      }
      Dimension labelSize = new Dimension(w, 20);
      for(Iterator it = labels.iterator(); it.hasNext(); ) {
         JLabel next = (JLabel) it.next();
         next.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 2));
         next.setPreferredSize(labelSize);
         next.setHorizontalAlignment(SwingConstants.TRAILING);
      }
      // launch panel labels
      labels.clear();
      labels.add(jarLabel);
      labels.add(classLabel);
      labels.add(argLabel);
      w = 0;
      for(Iterator it = labels.iterator(); it.hasNext(); ) {
         JLabel next = (JLabel) it.next();
         Rectangle2D rect = font.getStringBounds(next.getText()+" ", frc); //$NON-NLS-1$
         w = Math.max(w, (int) rect.getWidth()+1);
      }
      labelSize = new Dimension(w, 20);
      for(Iterator it = labels.iterator(); it.hasNext(); ) {
         JLabel next = (JLabel) it.next();
         next.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 2));
         next.setPreferredSize(labelSize);
         next.setHorizontalAlignment(SwingConstants.TRAILING);
      }
      labels.clear();
      // author panel labels
      labels.add(authorLabel);
      labels.add(keywordLabel);
      labels.add(levelLabel);
      labels.add(languagesLabel);
      w = 0;
      for(Iterator it = labels.iterator(); it.hasNext(); ) {
         JLabel next = (JLabel) it.next();
         Rectangle2D rect = font.getStringBounds(next.getText()+" ", frc); //$NON-NLS-1$
         w = Math.max(w, (int) rect.getWidth()+1);
      }
      labelSize = new Dimension(w, 20);
      for(Iterator it = labels.iterator(); it.hasNext(); ) {
         JLabel next = (JLabel) it.next();
         next.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 2));
         next.setPreferredSize(labelSize);
         next.setHorizontalAlignment(SwingConstants.TRAILING);
      }
   }

   /**
    * Refreshes the GUI.
    */
   protected void refreshGUI() {
      if(previousNode!=null) { // new tab has been selected
         LaunchNode prev = previousNode;
         previousNode = null;
         refreshNode(prev);
      }
      if(newNodeSelected) {
         argSpinner.setValue(new Integer(0));
         htmlSpinner.setValue(new Integer(0));
         newNodeSelected = false;
      }
      titleField.setText(title);
      titleField.setBackground(Color.white);
      super.refreshGUI();
      // refresh frame title
      String theTitle = frame.getTitle();
      if(this.title!=null) {
         if(!changedFiles.isEmpty()) {
            theTitle += " ["+tabSetName+"*]"; //$NON-NLS-1$ //$NON-NLS-2$
         } else {
            theTitle += " ["+tabSetName+"]";  //$NON-NLS-1$ //$NON-NLS-2$
         }
      } else if(!changedFiles.isEmpty()) {
         if(tabbedPane.getTabCount()==0) {
            changedFiles.clear();
         } else {
            theTitle += "*";                  //$NON-NLS-1$
         }
      }
      frame.setTitle(theTitle);
      // refresh selected node items
      final LaunchNode node = getSelectedNode();
      if(node!=null) {
         // refresh tab titles
         for(int i = 0; i<tabbedPane.getTabCount(); i++) {
            LaunchNode root = getTab(i).getRootNode();
            tabbedPane.setTitleAt(i, root.toString());
         }
         // refresh display tab
         hiddenCheckBox.setSelected(node.isHiddenInLauncher());
         boolean parentHidden = (node.getParent()!=null)&&((LaunchNode) node.getParent()).isHiddenInLauncher();
         hiddenCheckBox.setEnabled(!parentHidden);
         nameField.setText(node.toString());
         nameField.setBackground(Color.white);
         tooltipField.setText(node.tooltip);
         tooltipField.setBackground(Color.white);
         descriptionPane.setText(node.description);
         descriptionPane.setBackground(Color.white);
         int n = ((Integer) htmlSpinner.getValue()).intValue();
         LaunchNode.HTML html = node.getHTML(n);
         String urlPath = (html!=null) ? html.path : null;
         urlField.setText(urlPath);
         urlField.setBackground(((urlPath!=null)&&(html.url==null)) ? RED : Color.white);
         htmlSpinnerModel.setMaximum(new Integer(node.getHTMLCount()));
         htmlSpinner.setVisible(node.getHTML(0)!=null);
         boolean hasURL = (html!=null)&&(html.path!=null);
         htmlTabTitleLabel.setVisible(hasURL);
         htmlTabTitleField.setVisible(hasURL);
         htmlTabTitleField.setText((html!=null) ? html.title : null);
         htmlTabTitleField.setBackground(Color.white);
         if((html!=null)&&(html.url!=null)&&(htmlPane.getPage()!=html.url)) {
            try {
               if(html.url.getContent()!=null) {
                  final java.net.URL url = html.url;
                  Runnable runner = new Runnable() {

                     public void run() {
                        try {
                           htmlPane.setPage(url);
                        } catch(IOException ex) {
                           OSPLog.fine(LaunchRes.getString("Log.Message.BadURL")+" "+url); //$NON-NLS-1$ //$NON-NLS-2$
                        }
                     }
                  };
                  SwingUtilities.invokeLater(runner);
               }
            } catch(IOException ex) {
               htmlPane.setText(null);
            }
         } else if((html==null)||(html.url==null)) {
            htmlPane.setContentType(LaunchPanel.TEXT_TYPE);
            htmlPane.setText(null);
         }
         // refresh launch node
         // check the path and update the class chooser
         String path = node.getClassPath(); // in node-to-root order
         if(!path.equals(previousClassPath)) {
            boolean success = getClassChooser().setPath(path);
            searchJarAction.setEnabled(success);
         }
         // store path for later comparison
         previousClassPath = node.getClassPath();
         jarField.setText(node.classPath);
         jarField.setBackground(((node.classPath!=null)&&!getClassChooser().isLoaded(node.classPath))
                                ? RED : Color.white);
         classField.setText(node.launchClassName);
         classField.setBackground(((node.getLaunchClass()==null)&&(node.launchClassName!=null)) ? RED : Color.white);
         n = ((Integer) argSpinner.getValue()).intValue();
         if(node.args.length>n) {
            argField.setText(node.args[n]);
         } else {
            argField.setText(""); //$NON-NLS-1$
         }
         boolean xmlArg = argField.getText().endsWith(".xml"); //$NON-NLS-1$
         Resource res = null;
         if(xmlArg) {
            res = ResourceLoader.getResource(argField.getText());
            argField.setBackground((res==null) ? RED : Color.white);
         } else {
            argField.setBackground(Color.white);
         }
         // set enabled and selected states of checkboxes
         LaunchNode parent = (LaunchNode) node.getParent(); // may be null
         singletonCheckBox.setEnabled((parent==null)||!parent.isSingleton());
         singletonCheckBox.setSelected(node.isSingleton());
         singleVMCheckBox.setSelected(node.isSingleVM());
         if(node.isSingleVM()) {
            showLogCheckBox.setEnabled((parent==null)||!parent.isShowLog());
            showLogCheckBox.setSelected(node.isShowLog());
            clearLogCheckBox.setEnabled((parent==null)||!parent.isClearLog());
            clearLogCheckBox.setSelected(node.isClearLog());
            singleAppCheckBox.setEnabled(true);
            singleAppCheckBox.setSelected(node.isSingleApp());
         } else {
            showLogCheckBox.setEnabled(false);
            showLogCheckBox.setSelected(false);
            clearLogCheckBox.setEnabled(false);
            clearLogCheckBox.setSelected(false);
            singleAppCheckBox.setEnabled(false);
            singleAppCheckBox.setSelected(false);
         }
         levelDropDown.setVisible(node.isShowLog());
         clearLogCheckBox.setVisible(node.isShowLog());
         // refresh the level dropdown if visible
         if(levelDropDown.isVisible()) {
            boolean useAll = ((parent==null)||!parent.isShowLog());
            // disable during refresh to prevent triggering events
            levelDropDown.setEnabled(false);
            levelDropDown.removeAllItems();
            for(int i = 0; i<OSPLog.levels.length; i++) {
               if(useAll||(OSPLog.levels[i].intValue()<=parent.getLogLevel().intValue())) {
                  levelDropDown.addItem(OSPLog.levels[i]);
               }
            }
            levelDropDown.setSelectedItem(node.getLogLevel());
            levelDropDown.setEnabled(true);
         }
         // refresh author tab
         authorField.setText(node.getAuthor());
         authorField.setBackground(Color.white);
         keywordField.setText(node.keywords);
         keywordField.setBackground(Color.white);
         levelField.setText(node.level);
         levelField.setBackground(Color.white);
         languagesField.setText(node.languages);
         languagesField.setBackground(Color.white);
         commentPane.setText(node.comment);
         commentPane.setBackground(Color.white);
         boolean hasPW = (password!=null)&&!password.equals(""); //$NON-NLS-1$
         onLoadCheckBox.setEnabled(hasPW);
         onLoadCheckBox.setSelected(hasPW&&pwRequiredToLoad);
         encryptCheckBox.setSelected(password!=null);
         passwordEditor.setEnabled(encryptCheckBox.isSelected());
         passwordLabel.setEnabled(encryptCheckBox.isSelected());
         passwordEditor.setText(password);
         passwordEditor.setBackground(Color.white);
      }
      // rebuild file menu
      fileMenu.removeAll();
      fileMenu.add(newItem);
      if(undoManager.canReload()) {
         fileMenu.add(backItem);
      }
      fileMenu.addSeparator();
      fileMenu.add(openItem);
      if(openFromJarMenu!=null) {
         fileMenu.add(openFromJarMenu);
      }
      LaunchPanel tab = getSelectedTab();
      boolean isZipped = (jarBasePath!=null)&&!jarBasePath.equals(""); //$NON-NLS-1$
      saveAllItem.setEnabled(!isZipped&&isTabSetWritable());
      if(tab!=null) {
         fileMenu.add(importItem);
         fileMenu.addSeparator();
         fileMenu.add(closeTabItem);
         fileMenu.add(closeAllItem);
         fileMenu.addSeparator();
         fileMenu.add(previewItem);
         fileMenu.addSeparator();
         fileMenu.add(saveAllItem);
         fileMenu.add(saveSetAsItem);
         if(OSPRuntime.getLaunchJarName()!=null) {
            fileMenu.addSeparator();
            fileMenu.add(saveJarItem);
         }
         // added by W. Christian to capture screen
         JMenu printMenu = new JMenu("");                          //$NON-NLS-1$
         JMenuItem printFrameItem = new JMenuItem("");         //$NON-NLS-1$
         JMenuItem saveFrameAsEPSItem = new JMenuItem(""); //$NON-NLS-1$
         printMenu.add(printFrameItem);
         printMenu.add(saveFrameAsEPSItem);
         fileMenu.add(printMenu);
         // print action
         printFrameItem.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
               PrintUtils.printComponent(frame);
            }
         });
         // save as EPS action
         saveFrameAsEPSItem.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
               try {
                  PrintUtils.saveComponentAsEPS(frame);
               } catch(IOException ex) {/** empty block */}
            }
         });
         // end W. Christian code
         // set tab properties
         frame.getContentPane().add(toolbar, BorderLayout.NORTH);
         tab.dataPanel.add(editorTabs, BorderLayout.CENTER);
         // hidden root
         if(getRootNode().getChildCount()==0) {
            getRootNode().hiddenWhenRoot = false;
            hideRootCheckBox.setEnabled(false);
         } else {
            hideRootCheckBox.setEnabled((node==null)||!node.isButtonView());
         }
         boolean rootVisible = !getRootNode().hiddenWhenRoot;
         hideRootCheckBox.setSelected(!rootVisible);
         tab.tree.setRootVisible(rootVisible);
         if((getSelectedNode()==null)&&!rootVisible) {
            tab.setSelectedNode((LaunchNode) getRootNode().getChildAt(0));
         }
         // button view
         buttonViewCheckBox.setSelected((node!=null)&&node.isButtonView());
         buttonViewCheckBox.setEnabled(rootVisible);
         // editor enabled
         editorEnabledCheckBox.setSelected(editorEnabled);
      } else { // no tab
         frame.getContentPane().remove(toolbar);
      }
      if(exitItem!=null) {
         fileMenu.addSeparator();
         fileMenu.add(exitItem);
      }
      // update tabbed pane tool tips and icons
      for(int k = 0; k<tabbedPane.getTabCount(); k++) {
         LaunchNode root = getTab(k).getRootNode();
         if(root.getFileName()!=null) {
            tabbedPane.setIconAt(k, getFileIcon(root));
            tabbedPane.setToolTipTextAt(k, LaunchRes.getString("ToolTip.FileName")+" \""+root.getFileName()+"\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
         } else {
            tabbedPane.setIconAt(k, null);
            tabbedPane.setToolTipTextAt(k, null);
         }
      }
   }

   /**
    * Creates the GUI.
    *
    * @param splash true to show the splash screen
    */
   protected void createGUI(boolean splash) {
      wInit = 600;
      hInit = 540;
      labels = new ArrayList();
      super.createGUI(splash);
      // add listeners to refresh GUI
      frame.addWindowListener(new WindowAdapter() {

         // Added by W. Christian to disable the Language menu
         public void windowGainedFocus(WindowEvent e) {
            OSPRuntime.setAuthorMode(true);
         }

         public void windowActivated(WindowEvent e) {
            OSPRuntime.setAuthorMode(true);
         }

         public void windowOpened(WindowEvent e) {
            if(getSelectedNode()!=null) {
               // clear htmlPane to force a full refresh
               htmlPane.setContentType(LaunchPanel.TEXT_TYPE);
               htmlPane.setText(null);
            }
            refreshGUI();
         }
      });
      tabbedPane.addComponentListener(new ComponentAdapter() {

         public void componentResized(ComponentEvent e) {
            refreshGUI();
         }
      });
      // create additional file and folder icons
      String imageFile = "/org/opensourcephysics/resources/tools/images/whitefile.gif"; //$NON-NLS-1$
      whiteFileIcon = ResourceLoader.getIcon(imageFile);
      imageFile = "/org/opensourcephysics/resources/tools/images/ghostfile.gif";    //$NON-NLS-1$
      ghostFileIcon = ResourceLoader.getIcon(imageFile);
      imageFile = "/org/opensourcephysics/resources/tools/images/redfile.gif";      //$NON-NLS-1$
      redFileIcon = ResourceLoader.getIcon(imageFile);
      imageFile = "/org/opensourcephysics/resources/tools/images/yellowfile.gif";   //$NON-NLS-1$
      yellowFileIcon = ResourceLoader.getIcon(imageFile);
      imageFile = "/org/opensourcephysics/resources/tools/images/whitefolder.gif";  //$NON-NLS-1$
      whiteFolderIcon = ResourceLoader.getIcon(imageFile);
      imageFile = "/org/opensourcephysics/resources/tools/images/redfolder.gif";    //$NON-NLS-1$
      redFolderIcon = ResourceLoader.getIcon(imageFile);
      imageFile = "/org/opensourcephysics/resources/tools/images/greenfolder.gif";  //$NON-NLS-1$
      greenFolderIcon = ResourceLoader.getIcon(imageFile);
      imageFile = "/org/opensourcephysics/resources/tools/images/yellowfolder.gif"; //$NON-NLS-1$
      yellowFolderIcon = ResourceLoader.getIcon(imageFile);
      // create actions
      createActions();
      // create fields
      titleField = new JTextField();
      titleField.addKeyListener(new KeyAdapter() {

         public void keyPressed(KeyEvent e) {
            if(e.getKeyCode()==KeyEvent.VK_ENTER) {
               String text = titleField.getText();
               if(text.equals("")) { //$NON-NLS-1$
                  text = null;
               }
               if(text!=title) {
                  changedFiles.add(tabSetName);
               }
               title = text;
               refreshGUI();
            } else {
               titleField.setBackground(Color.yellow);
            }
         }
      });
      titleField.addFocusListener(new FocusAdapter() {

         public void focusLost(FocusEvent e) {
            String text = titleField.getText();
            if(text.equals("")) { //$NON-NLS-1$
               text = null;
            }
            if(text!=title) {
               changedFiles.add(tabSetName);
            }
            title = text;
            refreshGUI();
         }
      });
      nameField = new JTextField();
      nameField.addKeyListener(keyListener);
      nameField.addFocusListener(focusListener);
      tooltipField = new JTextField();
      tooltipField.addKeyListener(keyListener);
      tooltipField.addFocusListener(focusListener);
      htmlTitle = BorderFactory.createTitledBorder(LaunchRes.getString("Label.HTML")); //$NON-NLS-1$
      classField = new JTextField();
      classField.addKeyListener(keyListener);
      classField.addFocusListener(focusListener);
      argField = new JTextField();
      argField.addKeyListener(keyListener);
      argField.addMouseListener(new MouseAdapter() {

         public void mousePressed(MouseEvent e) {
            if(e.isPopupTrigger()||(e.getButton()==MouseEvent.BUTTON3)
                ||(e.isControlDown()&&(System.getProperty("os.name", "").indexOf("Mac")>-1))) {   //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
               XMLControl control = new XMLControlElement();
               if(control.read(argField.getText())!=null) {
                  JPopupMenu popup = new JPopupMenu();
                  JMenuItem item = new JMenuItem(LaunchRes.getString("MenuItem.EncryptionTool")); //$NON-NLS-1$
                  item.addActionListener(new ActionListener() {

                     public void actionPerformed(ActionEvent e) {
                        EncryptionTool tool = EncryptionTool.getTool();
                        tool.open(argField.getText());
                        tool.setVisible(true);
                     }
                  });
                  popup.add(item);
                  popup.show(argField, e.getX(), e.getY()+8);
               }
            }
         }
      });
      argField.addFocusListener(focusListener);
      jarField = new JTextField();
      jarField.addKeyListener(keyListener);
      jarField.addFocusListener(focusListener);
      urlField = new JTextField();
      urlField.addKeyListener(keyListener);
      urlField.addFocusListener(focusListener);
      htmlTabTitleField = new JTextField();
      htmlTabTitleField.addKeyListener(keyListener);
      htmlTabTitleField.addFocusListener(focusListener);
      keywordField = new JTextField();
      keywordField.addKeyListener(keyListener);
      keywordField.addFocusListener(focusListener);
      authorField = new JTextField();
      authorField.addKeyListener(keyListener);
      authorField.addFocusListener(focusListener);
      levelField = new JTextField();
      levelField.addKeyListener(keyListener);
      levelField.addFocusListener(focusListener);
      languagesField = new JTextField();
      languagesField.addKeyListener(keyListener);
      languagesField.addFocusListener(focusListener);
      commentPane = new JTextPane();
      commentPane.addKeyListener(keyListener);
      commentPane.addFocusListener(focusListener);
      commentScroller = new JScrollPane(commentPane);
      commentTitle = BorderFactory.createTitledBorder(LaunchRes.getString("Label.Comments")); //$NON-NLS-1$
      commentScroller.setBorder(commentTitle);
      descriptionPane = new JTextPane();
      descriptionPane.addKeyListener(keyListener);
      descriptionPane.addFocusListener(focusListener);
      descriptionScroller = new JScrollPane(descriptionPane);
      descriptionTitle = BorderFactory.createTitledBorder(LaunchRes.getString("Label.Description")); //$NON-NLS-1$
      descriptionScroller.setBorder(descriptionTitle);
      displaySplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT) {

         public void setDividerLocation(int loc) {
            super.setDividerLocation(loc);
            double divider = getDividerLocation();
            divider /= getHeight()-getDividerSize();
            setName(""+divider); //$NON-NLS-1$
         }

         public void setTopComponent(Component comp) {
            int prev = getLastDividerLocation();
            String divider = getName();
            super.setTopComponent(comp);
            if(divider!=null) {
               double loc = Double.parseDouble(divider);
               loc = Math.max(0.0, loc);
               loc = Math.min(1.0, loc);
               setDividerLocation(loc);
               setLastDividerLocation(prev);
               Runnable runner = new Runnable() {

                  public void run() {
                     JViewport view = descriptionScroller.getViewport();
                     view.setViewPosition(new Point(0, 0));
                  }
               };
               SwingUtilities.invokeLater(runner);
            }
         }
      };
      displaySplitPane.setBottomComponent(descriptionScroller);
      displaySplitPane.setOneTouchExpandable(true);
      displaySplitPane.setResizeWeight(0.5);
      htmlPane = new JTextPane();
      htmlPane.setEditable(false);
      htmlScroller = new JScrollPane(htmlPane);
      htmlScroller.setBorder(htmlTitle);
      displaySplitPane.setTopComponent(htmlScroller);
      hiddenCheckBox = new JCheckBox();
      hiddenCheckBox.addActionListener(changeAction);
      hiddenCheckBox.setContentAreaFilled(false);
      hiddenCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
      buttonViewCheckBox = new JCheckBox();
      buttonViewCheckBox.addActionListener(changeAction);
      buttonViewCheckBox.setContentAreaFilled(false);
      buttonViewCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
      // assemble display panel
      JPanel displayPanel = new JPanel(new BorderLayout());
      JToolBar nameBar = new JToolBar();
      nameBar.setFloatable(false);
      nameLabel = new JLabel();
      labels.add(nameLabel);
      nameBar.add(nameLabel);
      nameBar.add(nameField);
      nameBar.add(hiddenCheckBox);
      displayPanel.add(nameBar, BorderLayout.NORTH);
      JPanel tooltipPanel = new JPanel(new BorderLayout());
      displayPanel.add(tooltipPanel, BorderLayout.CENTER);
      JToolBar tooltipBar = new JToolBar();
      tooltipBar.setFloatable(false);
      tooltipLabel = new JLabel();
      tooltipBar.add(tooltipLabel);
      tooltipBar.add(tooltipField);
      tooltipPanel.add(tooltipBar, BorderLayout.NORTH);
      urlPanel = new JPanel(new BorderLayout());
      tooltipPanel.add(urlPanel, BorderLayout.CENTER);
      JToolBar urlBar = new JToolBar();
      urlBar.setFloatable(false);
      htmlLabel = new JLabel();
      urlLabel = new JLabel();
      urlLabel.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 2));
      htmlTabTitleLabel = new JLabel();
      htmlTabTitleLabel.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 2));
      htmlSpinnerModel = new SpinnerNumberModel(0, 0, 1, 1);
      htmlSpinner = new JSpinner(htmlSpinnerModel);
      JSpinner.NumberEditor editor = new JSpinner.NumberEditor(htmlSpinner);
      htmlSpinner.setEditor(editor);
      htmlSpinner.addChangeListener(new ChangeListener() {

         public void stateChanged(ChangeEvent e) {
            if(urlField.getBackground()==Color.yellow) {
               refreshSelectedNode();
            } else {
               refreshGUI();
            }
         }
      });
      urlBar.add(htmlLabel);
      urlBar.add(htmlSpinner);
      urlBar.add(urlLabel);
      urlBar.add(urlField);
      urlBar.add(openURLAction);
      urlBar.add(htmlTabTitleLabel);
      urlBar.add(htmlTabTitleField);
      urlPanel.add(urlBar, BorderLayout.NORTH);
      urlPanel.add(displaySplitPane, BorderLayout.CENTER);
      // assemble launch panel
      JPanel launchPanel = new JPanel(new BorderLayout());
      JToolBar jarBar = new JToolBar();
      jarBar.setFloatable(false);
      jarLabel = new JLabel();
      labels.add(jarLabel);
      jarBar.add(jarLabel);
      jarBar.add(jarField);
      jarBar.add(openJarAction);
      launchPanel.add(jarBar, BorderLayout.NORTH);
      JPanel classPanel = new JPanel(new BorderLayout());
      launchPanel.add(classPanel, BorderLayout.CENTER);
      JToolBar classBar = new JToolBar();
      classBar.setFloatable(false);
      classLabel = new JLabel();
      labels.add(classLabel);
      classBar.add(classLabel);
      classBar.add(classField);
      classBar.add(searchJarAction);
      classPanel.add(classBar, BorderLayout.NORTH);
      JPanel argPanel = new JPanel(new BorderLayout());
      classPanel.add(argPanel, BorderLayout.CENTER);
      JToolBar argBar = new JToolBar();
      argBar.setFloatable(false);
      argLabel = new JLabel();
      labels.add(argLabel);
      argBar.add(argLabel);
      SpinnerModel model = new SpinnerNumberModel(0, 0, maxArgs-1, 1);
      argSpinner = new JSpinner(model);
      editor = new JSpinner.NumberEditor(argSpinner);
      argSpinner.setEditor(editor);
      argSpinner.addChangeListener(new ChangeListener() {

         public void stateChanged(ChangeEvent e) {
            if(argField.getBackground()==Color.yellow) {
               refreshSelectedNode();
            } else {
               refreshGUI();
            }
         }
      });
      argBar.add(argSpinner);
      argBar.add(argField);
      argBar.add(openArgAction);
      argPanel.add(argBar, BorderLayout.NORTH);
      JPanel optionsPanel = new JPanel(new BorderLayout());
      argPanel.add(optionsPanel, BorderLayout.CENTER);
      JToolBar optionsBar = new JToolBar();
      optionsBar.setFloatable(false);
      singleVMCheckBox = new JCheckBox();
      singleVMCheckBox.addActionListener(changeAction);
      singleVMCheckBox.setContentAreaFilled(false);
      singleVMCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
      showLogCheckBox = new JCheckBox();
      showLogCheckBox.addActionListener(changeAction);
      showLogCheckBox.setContentAreaFilled(false);
      showLogCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
      clearLogCheckBox = new JCheckBox();
      clearLogCheckBox.addActionListener(changeAction);
      clearLogCheckBox.setContentAreaFilled(false);
      clearLogCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
      singletonCheckBox = new JCheckBox();
      singletonCheckBox.addActionListener(changeAction);
      singletonCheckBox.setContentAreaFilled(false);
      singletonCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
      singleAppCheckBox = new JCheckBox();
      singleAppCheckBox.addActionListener(changeAction);
      singleAppCheckBox.setContentAreaFilled(false);
      singleAppCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
      levelDropDown = new JComboBox(OSPLog.levels);
      levelDropDown.setMaximumSize(levelDropDown.getMinimumSize());
      levelDropDown.addActionListener(new ActionListener() {

         public void actionPerformed(ActionEvent e) {
            if(levelDropDown.isEnabled()) {
               LaunchNode node = getSelectedNode();
               if(node!=null) {
                  node.setLogLevel((Level) levelDropDown.getSelectedItem());
               }
            }
         }
      });
      Box checkBoxPanel = Box.createVerticalBox();
      JToolBar bar = new JToolBar();
      bar.setFloatable(false);
      bar.setAlignmentX(Component.LEFT_ALIGNMENT);
      bar.add(singletonCheckBox);
      bar.add(Box.createHorizontalGlue());
      checkBoxPanel.add(bar);
      bar = new JToolBar();
      bar.setFloatable(false);
      bar.setAlignmentX(Component.LEFT_ALIGNMENT);
      bar.add(singleVMCheckBox);
      bar.add(Box.createHorizontalGlue());
      checkBoxPanel.add(bar);
      bar = new JToolBar();
      bar.setFloatable(false);
      bar.setAlignmentX(Component.LEFT_ALIGNMENT);
      bar.add(singleAppCheckBox);
      bar.add(Box.createHorizontalGlue());
      checkBoxPanel.add(bar);
      bar = new JToolBar();
      bar.setFloatable(false);
      bar.add(showLogCheckBox);
      bar.add(Box.createHorizontalStrut(4));
      bar.add(levelDropDown);
      bar.add(Box.createHorizontalStrut(4));
      bar.add(clearLogCheckBox);
      bar.add(Box.createHorizontalGlue());
      bar.setAlignmentX(Component.LEFT_ALIGNMENT);
      checkBoxPanel.add(bar);
      optionsTitle = BorderFactory.createTitledBorder(LaunchRes.getString("Label.Options")); //$NON-NLS-1$
      Border recess = BorderFactory.createLoweredBevelBorder();
      optionsBar.setBorder(BorderFactory.createCompoundBorder(recess, optionsTitle));
      optionsBar.add(checkBoxPanel);
      optionsPanel.add(optionsBar, BorderLayout.NORTH);
      // create author tab
      JPanel authorPanel = new JPanel(new BorderLayout());
      // put author field on top
      JToolBar authorBar = new JToolBar();
      authorPanel.add(authorBar, BorderLayout.NORTH);
      authorBar.setFloatable(false);
      authorLabel = new JLabel();
      labels.add(authorLabel);
      authorBar.add(authorLabel);
      authorBar.add(authorField);
      // add keyword panel
      JPanel keywordPanel = new JPanel(new BorderLayout());
      authorPanel.add(keywordPanel, BorderLayout.CENTER);
      JToolBar keywordBar = new JToolBar();
      keywordPanel.add(keywordBar, BorderLayout.NORTH);
      keywordBar.setFloatable(false);
      keywordLabel = new JLabel();
      labels.add(keywordLabel);
      keywordBar.add(keywordLabel);
      keywordBar.add(keywordField);
      // add level panel
      JPanel levelPanel = new JPanel(new BorderLayout());
      keywordPanel.add(levelPanel, BorderLayout.CENTER);
      JToolBar levelBar = new JToolBar();
      levelPanel.add(levelBar, BorderLayout.NORTH);
      levelBar.setFloatable(false);
      levelLabel = new JLabel();
      labels.add(levelLabel);
      levelBar.add(levelLabel);
      levelBar.add(levelField);
      // add languages panel
      JPanel languagesPanel = new JPanel(new BorderLayout());
      levelPanel.add(languagesPanel, BorderLayout.CENTER);
      JToolBar languagesBar = new JToolBar();
      languagesPanel.add(languagesBar, BorderLayout.NORTH);
      languagesBar.setFloatable(false);
      languagesLabel = new JLabel();
      labels.add(languagesLabel);
      languagesBar.add(languagesLabel);
      languagesBar.add(languagesField);
      // put security panel in center
      JPanel securityPanel = new JPanel(new BorderLayout());
      languagesPanel.add(securityPanel, BorderLayout.CENTER);
      JToolBar securityBar = new JToolBar();
      securityBar.setFloatable(false);
      securityPanel.add(securityBar, BorderLayout.NORTH);
      securityTitle = BorderFactory.createTitledBorder(LaunchRes.getString("Label.Security")); //$NON-NLS-1$
      securityBar.setBorder(BorderFactory.createCompoundBorder(recess, securityTitle));
      // use vertical box to stack components in security panel
      Box securityBox = Box.createVerticalBox();
      securityBar.add(securityBox);
      // add editorEnabled to box
      bar = new JToolBar();
      bar.setFloatable(false);
      bar.setAlignmentX(Component.LEFT_ALIGNMENT);
      editorEnabledCheckBox = new JCheckBox();
      editorEnabledCheckBox.addActionListener(changeAction);
      editorEnabledCheckBox.setContentAreaFilled(false);
      editorEnabledCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
      bar.add(editorEnabledCheckBox);
      bar.add(Box.createHorizontalGlue());
      securityBox.add(bar);
      // add encrypt checkbox to box
      bar = new JToolBar();
      bar.setFloatable(false);
      bar.setAlignmentX(Component.LEFT_ALIGNMENT);
      encryptCheckBox = new JCheckBox();
      encryptCheckBox.addActionListener(new ActionListener() {

         public void actionPerformed(ActionEvent e) {
            if(encryptCheckBox.isSelected()&&(password==null)) {
               password = "";                                                  //$NON-NLS-1$
            } else if(!encryptCheckBox.isSelected()) {
               password = null;
            }
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangeEncrypted")); //$NON-NLS-1$
            if(tabSetName!=null) {
               changedFiles.add(tabSetName);
            }
            refreshGUI();
         }
      });
      encryptCheckBox.setContentAreaFilled(false);
      bar.add(encryptCheckBox);
      bar.add(Box.createHorizontalGlue());
      securityBox.add(bar);
      // add password field to box
      bar = new JToolBar();
      bar.setFloatable(false);
      bar.setAlignmentX(Component.LEFT_ALIGNMENT);
      passwordLabel = new JLabel();
      passwordLabel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 2));
      bar.add(passwordLabel);
      passwordEditor = new JTextField();
      passwordEditor.addKeyListener(new KeyAdapter() {

         public void keyPressed(KeyEvent e) {
            if(e.getKeyCode()==KeyEvent.VK_ENTER) {
               String text = passwordEditor.getText();
               if(text.equals("")&& //$NON-NLS-1$
                                  (!encryptCheckBox.isSelected()||!encryptCheckBox.isEnabled())) {
                  text = null;
               }
               if(text!=password) {
                  changedFiles.add(tabSetName);
               }
               password = text;
               refreshGUI();
            } else {
               passwordEditor.setBackground(Color.yellow);
            }
         }
      });
      bar.add(passwordEditor);
      bar.add(Box.createHorizontalGlue());
      onLoadCheckBox = new JCheckBox();
      onLoadCheckBox.addActionListener(new ActionListener() {

         public void actionPerformed(ActionEvent e) {
            pwRequiredToLoad = onLoadCheckBox.isSelected();
            OSPLog.finest(LaunchRes.getString("Log.Message.ChangePWRequirement")); //$NON-NLS-1$
            if(tabSetName!=null) {
               changedFiles.add(tabSetName);
            }
            refreshGUI();
         }
      });
      onLoadCheckBox.setContentAreaFilled(false);
      bar.add(onLoadCheckBox);
      securityBox.add(bar);
      securityPanel.add(commentScroller, BorderLayout.CENTER);
      // create the editor tabs
      editorTabs = new JTabbedPane(SwingConstants.TOP);
      editorTabs.addTab(LaunchRes.getString("Tab.Display"), displayPanel); //$NON-NLS-1$
      editorTabs.addTab(LaunchRes.getString("Tab.Launch"), launchPanel);   //$NON-NLS-1$
      editorTabs.addTab(LaunchRes.getString("Tab.Author"), authorPanel);   //$NON-NLS-1$
      // editorTabs.addTab(LaunchRes.getString("Tab.Server"), serverPanel);
      // create toolbar
      toolbar = new JToolBar();
      toolbar.setFloatable(false);
      toolbar.setRollover(true);
      toolbar.setBorder(BorderFactory.createLineBorder(Color.gray));
      frame.getContentPane().add(toolbar, BorderLayout.NORTH);
      newTabButton = new JButton(newTabAction);
      toolbar.add(newTabButton);
      addButton = new JButton(addAction);
      toolbar.add(addButton);
      cutButton = new JButton(cutAction);
      toolbar.add(cutButton);
      copyButton = new JButton(copyAction);
      toolbar.add(copyButton);
      pasteButton = new JButton(pasteAction);
      toolbar.add(pasteButton);
      moveUpButton = new JButton(moveUpAction);
      toolbar.add(moveUpButton);
      moveDownButton = new JButton(moveDownAction);
      toolbar.add(moveDownButton);
      titleLabel = new JLabel(LaunchRes.getString("Label.Title")); //$NON-NLS-1$
      titleLabel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 2));
      toolbar.add(titleLabel);
      toolbar.add(titleField);
      toolbar.add(buttonViewCheckBox);
      hideRootCheckBox = new JCheckBox();
      hideRootCheckBox.addActionListener(changeAction);
      hideRootCheckBox.setContentAreaFilled(false);
      toolbar.add(hideRootCheckBox);
      // create menu items
      int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
      // create new tabset item
      newItem = new JMenuItem(newTabSetAction);
      newItem.setAccelerator(KeyStroke.getKeyStroke('N', mask));
      // create preview item
      previewItem = new JMenuItem();
      previewItem.addActionListener(new ActionListener() {

         public void actionPerformed(ActionEvent e) {
            String base = tabSetBasePath;
            previewing = true;
            LaunchSet set = new LaunchSet(LaunchBuilder.this, tabSetName);
            XMLControlElement control = new XMLControlElement(set);
            control.setValue("filename", tabSetName); //$NON-NLS-1$
            Launcher launcher = new Launcher(control.toXML());
            LaunchNode node = LaunchBuilder.this.getSelectedNode();
            if(node!=null) {
               launcher.setSelectedNode(node.getPathString());
            }
            Point p = LaunchBuilder.this.frame.getLocation();
            launcher.frame.setLocation(p.x+24, p.y+24);
            launcher.frame.setVisible(true);
            // set close operation to dispose to distinguish from default hide
            launcher.frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
            tabSetBasePath = base;
            previewing = false;
            launcher.password = password;
            launcher.previewing = true;
            launcher.spawner = LaunchBuilder.this;
            launcher.refreshGUI();
         }
      });
      // create other menu items
      importItem = new JMenuItem(importAction);
      saveJarItem = new JMenuItem(saveJarAction);
      saveNodeItem = new JMenuItem(saveAction);
      saveNodeAsItem = new JMenuItem(saveAsAction);
      saveAllItem = new JMenuItem(saveAllAction);
      openTabItem = new JMenuItem(openTabAction);
      saveAllItem.setAccelerator(KeyStroke.getKeyStroke('S', mask));
      saveSetAsItem = new JMenuItem(saveSetAsAction);
      toolsMenu = new JMenu();
      frame.getJMenuBar().add(toolsMenu, 2);
      encryptionToolItem = new JMenuItem();
      toolsMenu.add(encryptionToolItem);
      encryptionToolItem.addActionListener(new ActionListener() {

         public void actionPerformed(ActionEvent e) {
            EncryptionTool.getTool().setVisible(true);
         }
      });
      // replace tab listener
      tabbedPane.removeMouseListener(tabListener);
      tabListener = new MouseAdapter() {

         public void mousePressed(MouseEvent e) {
            if(contentPane.getTopLevelAncestor()!=frame) {
               return;
            }
            if(e.isPopupTrigger()||(e.getButton()==MouseEvent.BUTTON3)
                ||(e.isControlDown()&&(System.getProperty("os.name", "").indexOf("Mac")>-1))) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
               // make popup and add items
               JPopupMenu popup = new JPopupMenu();
               JMenuItem item = new JMenuItem(LaunchRes.getString("MenuItem.Close"));           //$NON-NLS-1$
               item.addActionListener(new ActionListener() {

                  public void actionPerformed(ActionEvent e) {
                     removeSelectedTab();
                  }
               });
               popup.add(item);
               popup.addSeparator();
               item = new JMenuItem(LaunchRes.getString("Menu.File.SaveAs"));                   //$NON-NLS-1$
               item.addActionListener(new ActionListener() {

                  public void actionPerformed(ActionEvent e) {
                     LaunchNode node = getSelectedTab().getRootNode();
                     if(saveAs(node)!=null) {
                        int i = tabbedPane.getSelectedIndex();
                        tabbedPane.setTitleAt(i, node.toString());
                     }
                     refreshGUI();
                  }
               });
               popup.add(item);
               final int i = tabbedPane.getSelectedIndex();
               if((i>0)||(i<tabbedPane.getTabCount()-1)) {
                  popup.addSeparator();
               }
               if(i<tabbedPane.getTabCount()-1) {
                  item = new JMenuItem(LaunchRes.getString("Popup.MenuItem.MoveUp")); //$NON-NLS-1$
                  item.addActionListener(new ActionListener() {

                     public void actionPerformed(ActionEvent e) {
                        LaunchPanel tab = getSelectedTab();
                        LaunchNode root = tab.getRootNode();
                        LaunchBuilder.super.removeSelectedTab();
                        tabbedPane.insertTab(getDisplayName(root.getFileName()), null, tab, null, i+1);
                        tabbedPane.setSelectedComponent(tab);
                        tabs.add(i+1, tab);
                     }
                  });
                  popup.add(item);
               }
               if(i>0) {
                  item = new JMenuItem(LaunchRes.getString("Popup.MenuItem.MoveDown")); //$NON-NLS-1$
                  item.addActionListener(new ActionListener() {

                     public void actionPerformed(ActionEvent e) {
                        LaunchPanel tab = getSelectedTab();
                        LaunchNode root = tab.getRootNode();
                        LaunchBuilder.super.removeSelectedTab();
                        tabbedPane.insertTab(getDisplayName(root.getFileName()), null, tab, null, i-1);
                        tabbedPane.setSelectedComponent(tab);
                        tabs.add(i-1, tab);
                     }
                  });
                  popup.add(item);
               }
               popup.show(tabbedPane, e.getX(), e.getY()+8);
            }
         }
      };
      tabbedPane.addMouseListener(tabListener);
      frame.pack();
      displaySplitPane.setDividerLocation(0.7);
   }

   /**
    * Sets the font level.
    *
    * @param level the level
    */
   protected void setFontLevel(int level) {
      final int prev = displaySplitPane.getLastDividerLocation();
      final String divider = displaySplitPane.getName();
      // set font levels of titled borders
      FontSizer.setFonts(htmlTitle, level);
      FontSizer.setFonts(commentTitle, level);
      FontSizer.setFonts(descriptionTitle, level);
      FontSizer.setFonts(optionsTitle, level);
      FontSizer.setFonts(securityTitle, level);
      super.setFontLevel(level);
      if(divider!=null) {
         Runnable runner = new Runnable() {

            public void run() {
               double loc = Double.parseDouble(divider);
               loc = Math.max(0.0, loc);
               loc = Math.min(1.0, loc);
               displaySplitPane.setDividerLocation(loc);
               displaySplitPane.setLastDividerLocation(prev);
            }
         };
         SwingUtilities.invokeLater(runner);
      }
   }

   /**
    * Creates the actions.
    */
   protected void createActions() {
      String imageFile = "/org/opensourcephysics/resources/tools/images/open.gif"; //$NON-NLS-1$
      Icon openIcon = new ImageIcon(Launcher.class.getResource(imageFile));
      openJarAction = new AbstractAction(null, openIcon) {

         public void actionPerformed(ActionEvent e) {
            JFileChooser chooser = getJARChooser();
            int result = chooser.showOpenDialog(null);
            if(result==JFileChooser.APPROVE_OPTION) {
               File file = chooser.getSelectedFile();
               String newJar = XML.getRelativePath(file.getPath());
               String jars = jarField.getText();
               if(jars.indexOf(newJar)>-1) {
                  newJar = null;
               }
               if(!jars.equals("")) { //$NON-NLS-1$
                  jars += ";"; //$NON-NLS-1$
               }
               if(newJar!=null) {
                  jarField.setText(jars+newJar);
               }
               OSPRuntime.chooserDir = XML.getDirectoryPath(file.getPath());
               searchJarAction.setEnabled(true);
               refreshSelectedNode();
            }
         }
      };
      searchJarAction = new AbstractAction(null, openIcon) {

         public void actionPerformed(ActionEvent e) {
            LaunchNode node = getSelectedNode();
            if((node!=null)&&getClassChooser().chooseClassFor(node)) {
               if(node.getOwner()!=null) {
                  changedFiles.add(node.getOwner().getFileName());
               } else {
                  changedFiles.add(tabSetName);
               }
               refreshClones(node);
               refreshGUI();
            }
         }
      };
      searchJarAction.setEnabled(false);
      saveJarAction = new AbstractAction(LaunchRes.getString("LaunchBuilder.Action.CreateJar.Name")) { //$NON-NLS-1$
         public void actionPerformed(ActionEvent e) {
            // define base folder
            File base = new File(XML.getDirectoryPath(OSPRuntime.getLaunchJarPath()));
            String jarName = XML.stripExtension(LaunchBuilder.this.tabSetName)+".jar"; //$NON-NLS-1$
            File target = new File(base, jarName);
            // source list includes launch jar plus user-selected files/folders
            final ArrayList source = new ArrayList();
            JarTreeDialog jarChooser = new JarTreeDialog(frame, base);
            // in jarChooser, preselect launch jar and all non-zip/jar child files
            source.add(OSPRuntime.getLaunchJarName());   // launch jar added first
            // add files in accept method
            base.listFiles(new java.io.FileFilter() {

               public boolean accept(File f) {
                  // skip jars, zip files and other undesirables
                  if (f.getName().endsWith(".jar") ||          //$NON-NLS-1$
                      f.getName().endsWith(".zip") ||          //$NON-NLS-1$
                      f.getName().endsWith(".DS_Store") ||     //$NON-NLS-1$
                      f.getName().endsWith(".localized") ||     //$NON-NLS-1$
                      f.getName().endsWith(".tmp")) {          //$NON-NLS-1$
                     return false;
                  }
                  // file is accepted, so add to source list
                  source.add(f.getName());
                  return true;
               }
            });
            jarChooser.setSelectionRelativePaths((String[]) source.toArray(new String[0]));
            jarChooser.setVisible(true);
            String[] paths = jarChooser.getSelectionRelativePaths();
            if(paths==null) return;
            source.clear();
            // add paths to source list
            for(int i = 0; i<paths.length; i++) {
               if(paths[i].equals(OSPRuntime.getLaunchJarName())) {
                  // put launch jar first so later paths will overwrite
                  source.add(0, paths[i]);
               } else {
                  source.add(paths[i]);
               }
            }
            java.util.jar.Manifest manifest = JarTool.createManifest("", "org.opensourcephysics.tools.Launcher"); //$NON-NLS-1$ //$NON-NLS-2$
            JarTool.alwaysOverwrite();    // overwrite all with no warning
            JarTool.setOwnerFrame(frame); // set owner of progress dialog
            JarTool.getTool().create(source, base, target, manifest);
         }
      };
      openArgAction = new AbstractAction(null, openIcon) {

         public void actionPerformed(ActionEvent e) {
            JFileChooser chooser = getFileChooser();
            int result = chooser.showOpenDialog(null);
            if(result==JFileChooser.APPROVE_OPTION) {
               File file = chooser.getSelectedFile();
               argField.setText(XML.getRelativePath(file.getPath()));
               OSPRuntime.chooserDir = XML.getDirectoryPath(file.getPath());
               refreshSelectedNode();
            }
         }
      };
      openURLAction = new AbstractAction(null, openIcon) {

         public void actionPerformed(ActionEvent e) {
            JFileChooser chooser = getHTMLChooser();
            int result = chooser.showOpenDialog(null);
            if(result==JFileChooser.APPROVE_OPTION) {
               File file = chooser.getSelectedFile();
               urlField.setText(XML.getRelativePath(file.getPath()));
               OSPRuntime.chooserDir = XML.getDirectoryPath(file.getPath());
               refreshSelectedNode();
            }
         }
      };
      openTabAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            LaunchNode node = getSelectedNode();
            String tabName = node.toString();
            for(int i = 0; i<tabbedPane.getComponentCount(); i++) {
               if(tabbedPane.getTitleAt(i).equals(tabName)) {
                  tabbedPane.setSelectedIndex(i);
                  return;
               }
            }
            XMLControl control = new XMLControlElement(node);
            XMLControl cloneControl = new XMLControlElement(control);
            LaunchNode clone = (LaunchNode) cloneControl.loadObject(null);
            clone.setFileName(node.getFileName());
            addTab(clone);
            refreshGUI();
         }
      };
      changeAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            refreshSelectedNode();
         }
      };
      newTabSetAction = new AbstractAction() {
         public void actionPerformed(ActionEvent e) {
        	 String[] prevArgs = undoManager.getLauncherState();
           if(removeAllTabs()) {
             if(prevArgs != null) {
            	 // null new args indicate not redoable
               LauncherUndo.LoadEdit edit =
              	 undoManager.new LoadEdit(null, prevArgs);
               undoSupport.postEdit(edit);
             }
             LaunchNode root = new LaunchNode(LaunchRes.getString("NewTab.Name")); //$NON-NLS-1$
             addTab(root);
           }
         }
      };
      addAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            LaunchNode newNode = new LaunchNode(LaunchRes.getString("NewNode.Name")); //$NON-NLS-1$
            addChildToSelectedNode(newNode);
         }
      };
      newTabAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            LaunchNode root = new LaunchNode(LaunchRes.getString("NewTab.Name")); //$NON-NLS-1$
            addTab(root);
         }
      };
      cutAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            copyAction.actionPerformed(null);
            removeSelectedNode();
         }
      };
      copyAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            LaunchNode node = getSelectedNode();
            if(node!=null) {
               XMLControl control = new XMLControlElement(node);
               control.setValue("filename", node.getFileName()); //$NON-NLS-1$
               StringSelection data = new StringSelection(control.toXML());
               Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
               clipboard.setContents(data, data);
            }
         }
      };
      pasteAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            try {
               Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
               Transferable data = clipboard.getContents(null);
               String dataString = (String) data.getTransferData(DataFlavor.stringFlavor);
               if(dataString!=null) {
                  XMLControlElement control = new XMLControlElement();
                  control.readXML(dataString);
                  if(control.getObjectClass()==LaunchNode.class) {
                     String fileName = control.getString("filename"); //$NON-NLS-1$
                     LaunchNode newNode = (LaunchNode) control.loadObject(null);
                     newNode.setFileName(fileName);
                     addChildToSelectedNode(newNode);
                  }
               }
            } catch(Exception ex) {
               ex.printStackTrace();
            }
         }
      };
      importAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            getXMLChooser().setFileFilter(xmlFileFilter);
            int result = getXMLChooser().showOpenDialog(null);
            if(result==JFileChooser.APPROVE_OPTION) {
               // open the file in an xml control
               File file = getXMLChooser().getSelectedFile();
               String fileName = file.getAbsolutePath();
               OSPRuntime.chooserDir = XML.getDirectoryPath(file.getPath());
               XMLControlElement control = new XMLControlElement(fileName);
               if(control.failedToRead()) {
                  OSPLog.info(LaunchRes.getString("Log.Message.InvalidXML")+" "+fileName);              //$NON-NLS-1$ //$NON-NLS-2$
                  JOptionPane.showMessageDialog(
                     null, LaunchRes.getString("Dialog.InvalidXML.Message")+" \""+XML.getName(fileName)+"\"", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                     LaunchRes.getString("Dialog.InvalidXML.Title"), JOptionPane.WARNING_MESSAGE); //$NON-NLS-1$
                  return;
               }
               if(control.getObjectClass()==LaunchNode.class) {
                  // add the child node
                  LaunchNode child = (LaunchNode) control.loadObject(null);
                  child.setFileName(fileName);
                  addChildToSelectedNode(child);
               } else {
                  OSPLog.info(LaunchRes.getString("Log.Message.NotLauncherFile")+" "+fileName);         //$NON-NLS-1$ //$NON-NLS-2$
                  JOptionPane.showMessageDialog(
                     null, LaunchRes.getString("Dialog.NotLauncherFile.Message")+" \""+XML.getName(fileName)+"\"", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                     LaunchRes.getString("Dialog.NotLauncherFile.Title"), JOptionPane.WARNING_MESSAGE); //$NON-NLS-1$
               }
            }
         }
      };
      saveAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            LaunchNode node = getSelectedNode();
            if(node.getFileName()!=null) {
               save(node, node.getFileName());
               refreshGUI();
            }
         }
      };
      saveAsAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            LaunchNode node = getSelectedNode();
            LaunchNode parent = (LaunchNode) node.getParent();
            String fileName = saveAs(node);
            if(fileName!=null) {
               // set ancestors to self-contained=false so this node will be saved correctly
               selfContained = false;
               Enumeration en = node.pathFromAncestorEnumeration(node.getRoot());
               while(en.hasMoreElements()) {
                  LaunchNode next = (LaunchNode) en.nextElement();
                  next.setSelfContained(false);
                  next.parentSelfContained = false;
               }
               if(parent!=null) {
                  if(parent.getOwner()!=null) {
                     changedFiles.add(parent.getOwner().getFileName());
                  }
                  refreshClones(parent);
               }
            }
            refreshGUI();
         }
      };
      saveAllAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            if(tabSetName.equals(LaunchRes.getString("Tabset.Name.New"))) { //$NON-NLS-1$
               saveTabSetAs();
            } else {
               saveTabSet();
            }
            refreshGUI();
         }
      };
      saveSetAsAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            saveTabSetAs();
            refreshGUI();
         }
      };
      moveUpAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            LaunchNode node = getSelectedNode();
            if(node==null) {
               return;
            }
            LaunchNode parent = (LaunchNode) node.getParent();
            if(parent==null) {
               return;
            }
            int i = parent.getIndex(node);
            if(i>0) {
               getSelectedTab().treeModel.removeNodeFromParent(node);
               getSelectedTab().treeModel.insertNodeInto(node, parent, i-1);
               getSelectedTab().setSelectedNode(node);
               if(parent.getOwner()!=null) {
                  changedFiles.add(parent.getOwner().getFileName());
               } else {
                  changedFiles.add(tabSetName);
               }
               refreshGUI();
            }
         }
      };
      moveDownAction = new AbstractAction() {

         public void actionPerformed(ActionEvent e) {
            LaunchNode node = getSelectedNode();
            if(node==null) {
               return;
            }
            LaunchNode parent = (LaunchNode) node.getParent();
            if(parent==null) {
               return;
            }
            int i = parent.getIndex(node);
            int end = parent.getChildCount();
            if(i<end-1) {
               getSelectedTab().treeModel.removeNodeFromParent(node);
               getSelectedTab().treeModel.insertNodeInto(node, parent, i+1);
               getSelectedTab().setSelectedNode(node);
               if(parent.getOwner()!=null) {
                  changedFiles.add(parent.getOwner().getFileName());
               } else {
                  changedFiles.add(tabSetName);
               }
               refreshGUI();
            }
         }
      };
      // create focus listener
      focusListener = new FocusAdapter() {

         public void focusLost(FocusEvent e) {
            refreshSelectedNode();
            refreshGUI();
         }
      };
      // create key listener
      keyListener = new KeyAdapter() {

         public void keyPressed(KeyEvent e) {
            JComponent comp = (JComponent) e.getSource();
            if((e.getKeyCode()==KeyEvent.VK_ENTER)
                &&(((comp!=descriptionPane)&&(comp!=commentPane))||e.isControlDown()||e.isShiftDown())) {
               refreshSelectedNode();
               refreshGUI();
            } else {
               comp.setBackground(Color.yellow);
            }
         }
      };
   }

   /**
    * Removes the selected node.
    */
   public void removeSelectedNode() {
      LaunchNode node = getSelectedNode();
      if((node==null)||(node.getParent()==null)) {
         return;
      }
      LaunchNode parent = (LaunchNode) node.getParent();
      getSelectedTab().treeModel.removeNodeFromParent(node);
      getSelectedTab().setSelectedNode(parent);
      if(parent.getOwner()!=null) {
         changedFiles.add(parent.getOwner().getFileName());
      } else {
         changedFiles.add(tabSetName);
      }
      refreshClones(parent);
      refreshGUI();
   }

   /**
    * Adds a child node to the selected node.
    *
    * @param child the child node to add
    */
   public void addChildToSelectedNode(LaunchNode child) {
      LaunchNode parent = getSelectedNode();
      if((parent!=null)&&(child!=null)) {
         LaunchNode[] nodes = child.getAllOwnedNodes();
         for(int i = 0; i<nodes.length; i++) {
            LaunchNode node = getSelectedTab().getClone(nodes[i]);
            if(node!=null) {
               getSelectedTab().setSelectedNode(node);
               JOptionPane.showMessageDialog(frame,
                                             LaunchRes.getString("Dialog.DuplicateNode.Message")+" \""+node+"\"", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                                             LaunchRes.getString("Dialog.DuplicateNode.Title"), //$NON-NLS-1$
                                             JOptionPane.WARNING_MESSAGE);
               return;
            }
         }
         getSelectedTab().treeModel.insertNodeInto(child, parent, parent.getChildCount());
         getSelectedTab().tree.scrollPathToVisible(new TreePath(child.getPath()));
         child.setLaunchClass(child.launchClassName);
         if(parent.getOwner()!=null) {
            changedFiles.add(parent.getOwner().getFileName());
         } else {
            changedFiles.add(tabSetName);
         }
         refreshClones(parent);
         refreshGUI();
      }
   }

   /**
    * Replaces clones of a specified node with new clones.
    *
    * @param node the current version of the node to clone
    */
   protected void refreshClones(LaunchNode node) {
      Map clones = getClones(node);
      replaceClones(node, clones);
   }

   /**
    * Replaces nodes with clones of the specified node.
    *
    * @param node the current version of the node to clone
    * @param clones the current clones to replace
    */
   protected void replaceClones(LaunchNode node, Map clones) {
      // find clones
      if(clones.isEmpty()) {
         return;
      }
      // replace clones
      XMLControl control = new XMLControlElement(node.getOwner());
      Iterator it = clones.keySet().iterator();
      while(it.hasNext()) {
         LaunchPanel tab = (LaunchPanel) it.next();
         LaunchNode clone = (LaunchNode) clones.get(tab);
         LaunchNode parent = (LaunchNode) clone.getParent();
         boolean expanded = tab.tree.isExpanded(new TreePath(clone.getPath()));
         if(parent!=null) {
            int index = parent.getIndex(clone);
            tab.treeModel.removeNodeFromParent(clone);
            clone = (LaunchNode) new XMLControlElement(control).loadObject(null);
            clone.setFileName(node.getFileName());
            tab.treeModel.insertNodeInto(clone, parent, index);
         } else {
            clone = (LaunchNode) new XMLControlElement(control).loadObject(null);
            clone.setFileName(node.getFileName());
            tab.treeModel.setRoot(clone);
         }
         if(expanded) {
            tab.tree.expandPath(new TreePath(clone.getPath()));
         }
      }
   }

   /**
    * Returns clones containing a specified node in a tab-to-node map.
    *
    * @param node the node
    * @return the tab-to-node map
    */
   protected Map getClones(LaunchNode node) {
      Map clones = new HashMap();
      // find clones
      node = node.getOwner();
      if(node==null) {
         return clones;
      }
      Component[] tabs = tabbedPane.getComponents();
      for(int i = 0; i<tabs.length; i++) {
         LaunchPanel tab = (LaunchPanel) tabs[i];
         LaunchNode clone = tab.getClone(node);
         if((clone!=null)&&(clone!=node)) {
            clones.put(tab, clone);
         }
      }
      return clones;
   }

   /**
    * Shows the about dialog.
    */
   protected void showAboutDialog() {
      String newline = XML.NEW_LINE;
      String aboutString = "LaunchBuilder "+version+" "+releaseDate+newline //$NON-NLS-1$ //$NON-NLS-2$
          +"Open Source Physics Project"+newline             //$NON-NLS-1$
          +"www.opensourcephysics.org";                      //$NON-NLS-1$
      JOptionPane.showMessageDialog(frame, aboutString, LaunchRes.getString("Help.About.Title")+" LaunchBuilder", //$NON-NLS-1$ //$NON-NLS-2$
                                    JOptionPane.INFORMATION_MESSAGE);
   }

   /**
    * Gets a file chooser for selecting jar files.
    *
    * @return the jar chooser
    */
   protected static JFileChooser getJARChooser() {
      getFileChooser().setFileFilter(jarFileFilter);
      return fileChooser;
   }

   /**
    * Gets a file chooser for selecting html files.
    *
    * @return the html chooser
    */
   protected static JFileChooser getHTMLChooser() {
      getFileChooser().setFileFilter(htmlFileFilter);
      return fileChooser;
   }

   /**
    * Gets a file chooser.
    *
    * @return the file chooser
    */
   protected static JFileChooser getFileChooser() {
      if(fileChooser==null) {
         fileChooser = new JFileChooser(new File(OSPRuntime.chooserDir));
         allFileFilter = fileChooser.getFileFilter();
         // create jar file filter
         jarFileFilter = new javax.swing.filechooser.FileFilter() {

            // accept all directories and *.jar files.
            public boolean accept(File f) {
               if(f==null) {
                  return false;
               }
               if(f.isDirectory()) {
                  return true;
               }
               String extension = null;
               String name = f.getName();
               int i = name.lastIndexOf('.');
               if((i>0)&&(i<name.length()-1)) {
                  extension = name.substring(i+1).toLowerCase();
               }
               if((extension!=null)&&extension.equals("jar")) {                 //$NON-NLS-1$
                  return true;
               }
               return false;
            }

            // the description of this filter
            public String getDescription() {
               return LaunchRes.getString("FileChooser.JarFilter.Description"); //$NON-NLS-1$
            }
         };
         // create html file filter
         htmlFileFilter = new javax.swing.filechooser.FileFilter() {

            // accept all directories, *.htm and *.html files.
            public boolean accept(File f) {
               if(f==null) {
                  return false;
               }
               if(f.isDirectory()) {
                  return true;
               }
               String extension = null;
               String name = f.getName();
               int i = name.lastIndexOf('.');
               if((i>0)&&(i<name.length()-1)) {
                  extension = name.substring(i+1).toLowerCase();
               }
               if((extension!=null)&&(extension.equals("htm")||extension.equals("html"))) { //$NON-NLS-1$ //$NON-NLS-2$
                  return true;
               }
               return false;
            }

            // the description of this filter
            public String getDescription() {
               return LaunchRes.getString("FileChooser.HTMLFilter.Description"); //$NON-NLS-1$
            }
         };
      }
      fileChooser.removeChoosableFileFilter(jarFileFilter);
      fileChooser.removeChoosableFileFilter(htmlFileFilter);
      fileChooser.setFileFilter(allFileFilter);
      return fileChooser;
   }

   /**
    * Handles a mouse pressed event.
    *
    * @param e the mouse event
    * @param tab the launch panel triggering the event
    */
   protected void handleMousePressed(MouseEvent e, final LaunchPanel tab) {
      super.handleMousePressed(e, tab);
      if(e.isPopupTrigger()||(e.getButton()==MouseEvent.BUTTON3)
          ||(e.isControlDown()&&(System.getProperty("os.name", "").indexOf("Mac")>-1))) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
         TreePath path = tab.tree.getPathForLocation(e.getX(), e.getY());
         if(path==null) {
            return;
         }
         final LaunchNode node = getSelectedNode();
         if(node==null) {
            return;
         }
         // add items to popup
         String fileName = node.getFileName();
         if((fileName!=null)&&changedFiles.contains(fileName)) {
            // add saveNode item
            if(popup.getComponentCount()!=0) {
               popup.addSeparator();
            }
            popup.add(saveNodeItem);
         }
         // add saveNodeAs item
         if(popup.getComponentCount()!=0) {
            popup.addSeparator();
         }
         popup.add(saveNodeAsItem);
         // add openTab item
         if(!node.isRoot()) {
            popup.addSeparator();
            openTabItem.setText(LaunchRes.getString("Action.OpenTab"));                   //$NON-NLS-1$
            popup.add(openTabItem);
         }
         popup.show(tab, e.getX()+4, e.getY()+12);
      }
   }

   /**
    * Overrides Launcher exit method.
    */
   protected void exit() {
      OSPRuntime.setAuthorMode(false);
      if(!saveAllChanges()) {
         // change default close operation to prevent window closing
         final int op = frame.getDefaultCloseOperation();
         frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
         // restore default close operation
         Runnable runner = new Runnable() {

            public void run() {
               frame.setDefaultCloseOperation(op);
            }
         };
         SwingUtilities.invokeLater(runner);
         return;
      }
      super.exit();
   }

   /**
    * Returns true if tabset is writable.
    *
    * @return true if writable
    */
   protected boolean isTabSetWritable() {
      String path = XML.getResolvedPath(tabSetName, tabSetBasePath);
      Resource res = ResourceLoader.getResource(path);
      File file = (res==null) ? null : res.getFile();
      boolean writable = (file==null) ? true : file.canWrite();
      if(!selfContained) {
         for(int i = 0; i<tabbedPane.getTabCount(); i++) {
            LaunchNode root = getTab(i).getRootNode();
            writable = writable&&isNodeWritable(root);
         }
      }
      return writable;
   }

   /**
    * Returns true if node is writable.
    *
    * @param node the node
    * @return true if writable
    */
   protected boolean isNodeWritable(LaunchNode node) {
      File file = node.getFile();
      boolean writable = (file==null) ? true : file.canWrite();
      // check node's owned child nodes if not self contained
      if(!node.isSelfContained()) {
         LaunchNode[] nodes = node.getChildOwnedNodes();
         for(int i = 0; i<nodes.length; i++) {
            writable = writable&&isNodeWritable(nodes[i]);
         }
      }
      return writable;
   }
}
/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.
 *
 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */
